上一篇我們把 LLM Inference 拆成了兩個階段:
Prompt → Prefill → Decode → Token → Decode → Token → ...
其中 Prefill 要一次處理大量 Token,而 Decode 則是一個 Token、一個 Token 慢慢往外生。
但這裡有個很奇怪的地方。
現在的 GPU 明明已經快得很誇張了。
動不動就是幾十、甚至上百 TFLOPS 的算力。
那為什麼跑 LLM 的時候,Token 還是有可能慢慢擠出來?
問題可能根本不是:
GPU 算不動。
而是:
資料送得不夠快。
可以把 GPU 想成一間超級大的中央廚房。
裡面有幾千個廚師,而且每個人手速都超快。
理論上一秒可以做非常多道菜。
但如果食材還放在倉庫裡呢?
廚師只能站在原地等。
廚師再多,也不會讓貨車開得比較快。
GPU 也是一樣。
要做運算之前,資料得先從記憶體搬進來。
大概可以想成:
GPU Memory → 搬資料 → GPU Core → 運算
所以 GPU 效能其實有兩個很重要的東西:
算得有多快
以及
資料搬得有多快
前者通常看 Compute。
後者就是今天的主角:
Memory Bandwidth
簡單來說:
記憶體每秒能搬多少資料給 GPU。
例如一張 GPU 有很強的運算能力,但 Memory Bandwidth 不夠。
就很像:
廚房有 1000 個廚師
但倉庫只有一台小貨車
最後大家還是在等貨。
這就是為什麼有時候你會看到:
GPU Utilization 看起來沒有滿,但模型就是快不起來。
不是 GPU 偷懶。
它可能只是在等資料。
關係非常大。
LLM 的模型參數可能有幾十億、甚至幾百億個。
而在 Decode 階段,每產生一個新的 Token,都需要用到模型裡大量的權重。
所以事情有點像:
讀模型權重 → 算下一個 Token → 再讀模型權重 → 再算下一個 Token → ...
問題來了。
Decode 一次只處理少量的新 Token。
也就是:
搬了一大堆資料,最後只生出一點點東西。
這時候 GPU 很容易不是卡在「算不完」。
而是卡在:
權重還沒搬完。
這種情況通常就會被稱為:
Memory-Bound
也就是效能主要被記憶體頻寬限制。
Prefill 一次會處理很多 Token。
例如:
你一次丟了 4000 個 Token 的 Prompt
GPU 可以一次拿這批資料做大量矩陣運算。
同一份權重也有更多機會被拿來做計算。
所以 Prefill 通常比較容易把 GPU 的計算能力吃起來。
可以很粗略地記成:
Prefill → 比較容易 Compute-Bound
Decode → 比較容易 Memory-Bound
當然,這不是永遠固定的。
Batch Size、模型大小、量化方式、GPU 型號都會改變結果。
但在理解 LLM Inference 時,先抓住這個方向就夠了。
假設現在只有一個人在問問題。
GPU 搬了一次模型權重,可能只拿來算這一個 Request。
很浪費。
但如果現在同時有很多人在問:
Request A + Request B + Request C + Request D
GPU 就有機會在一次運算裡,同時幫很多 Request 算下一個 Token。
可以想成:
原本貨車送一車食材,只服務一桌客人。
現在同一車食材,可以同時服務十桌。
突然就划算很多。
這也是為什麼後面我們會看到:
Batching
甚至:
Continuous Batching
它們會直接影響 LLM 的吞吐量。
如果你的 LLM Tokens/s 不如預期,不一定是 GPU 算力太差。
有時候真正卡住你的,是:
Memory Bandwidth
也就是 GPU 算得很快,但資料根本來不及送過去。
所以今天先記住:
Compute-Bound:GPU 算不完
Memory-Bound:資料搬不完
而 LLM 的 Decode,很多時候就是後者。
現在我們知道,Decode 每產生一個 Token,都得處理大量資料。
但還有另一個東西,會隨著 Context 越來越長,不斷吃掉你的顯存。
而且它幾乎每個 LLM Inference Engine 都得想辦法處理。
它就是:
KV Cache
下一篇我們就來看看:
明明只是多聊幾句話,為什麼顯存越吃越多?